#!/usr/bin/perl
# vim: nowrap ts=8 sw=4 cin ai
use Storable qw(freeze thaw);
use Digest::MD5 qw(md5_hex);
use DBI;
use strict;

sub _dbh
{
    DBI->connect_cached("DBI:mysql:notmac", "root", "", { RaiseError=>1, PrintError=>1 });
}

sub _check_credentials
{
    my ($username, $password) = @_;

    my $account = _dbh()->selectrow_hashref("SELECT * FROM accounts WHERE username=?", { Slice=>{} }, $username)
	or return 0;

    return $account if crypt($password, $account->{password}) eq $account->{password};
    return 0;
}

sub _open_session
{
    my ($username, $cid) = @_;

    my $sid = md5_hex(time(). {}. rand(). $$);

    _dbh()->do(<<EOSQL, undef, $cid, $sid, $username) + 0
UPDATE accounts SET run=run+1, cid=?, sid=?, sid_access=UNIX_TIMESTAMP()
 WHERE username=? AND (sid IS NULL OR sid_access<UNIX_TIMESTAMP()-300)
EOSQL
	or die "Another client is already syncing, please try again later.\n";

    my $session = _dbh()->selectrow_hashref("SELECT * FROM accounts WHERE sid=?", undef, $sid)
	or die "Error fetching session.\n";

    return $session;
}

sub _check_session
{
    my ($sid) = @_;

    _dbh()->do("UPDATE accounts SET sid_access=UNIX_TIMESTAMP() WHERE sid=?", undef, $sid) + 0
	or return;

    return _dbh()->selectrow_hashref("SELECT * FROM accounts WHERE sid=?", undef, $sid);
}

sub validate_account
{
    my $params = shift();

    die "Invalid request.\n" unless
	$params->{username} =~ /^.+$/ and
	$params->{password} =~ /^.+$/;

    die "Invalid credentials.\n" unless
	_check_credentials($params->{username}, $params->{password});

    return;
}

sub query_storage_info
{
    my $params = shift();

    die "Invalid request.\n" unless
	$params->{username} =~ /^.+$/ and
	$params->{password} =~ /^.+$/;

    my $account = _check_credentials($params->{username}, $params->{password})
	or die "Invalid credentials.\n";

    return {
	quota => 1024*1024*1024,
	quotaUsed => int(24.1*1024*1024),
	publicAccess => $account->{public_access},
	publicPasswordSet => $account->{public_password} ? 1 : 0,
    };
}

sub update_storage_info
{
    my $params = shift();

    die "Invalid request.\n" unless
	$params->{username} =~ /^.+$/ and
	$params->{password} =~ /^.+$/ and
	$params->{publicAccess} =~ /^(|read-only|read-write)$/ and
	$params->{publicPassword} =~ /^(|[a-zA-Z0-9]{6,8})$/;

    die "Invalid credentials.\n" unless
	_check_credentials($params->{username}, $params->{password});

    my %field_map = (publicAccess => 'public_access', publicPassword => 'public_password');
    my @keys = map $field_map{$_}, grep length $params->{$_}, sort keys %field_map;
    my @vals = map $params->{$_}, @keys;

    if (@keys)
    {
	_dbh()->do("UPDATE accounts SET @{[ join(',', map $_.'=?', @keys) ]} WHERE username=?", undef, @vals)
	    or die "Error updating information.\n";
    }

    return;
}

sub open_session
{
    my $params = shift();

    die "Invalid request.\n" unless
	$params->{username} =~ /^.+$/ and
	$params->{password} =~ /^.+$/ and
	$params->{clientIdentifier} =~ /^.+$/;

    die "Invalid credentials.\n" unless
	_check_credentials($params->{username}, $params->{password});

    my $session = _open_session($params->{username}, $params->{clientIdentifier})
	or die "Error opening session.\n";

    return {
	sid => $session->{sid},
	run => $session->{run},
    };
}

sub close_session
{
    my $params = shift();

    die "Invalid request.\n" unless
	$params->{sid} =~ /^[a-f0-9]{32}$/;

    my $session = _check_session($params->{sid})
	or die "Invalid session.\n";

    _dbh()->do("UPDATE accounts SET cid=NULL, sid=NULL, sid_access=NULL WHERE sid=?", undef, $params->{sid}) + 0
	or die "Error closing session.\n";

    return;
}

sub get_records
{
    my $params = shift();

    die "Invalid request.\n" unless
	$params->{sid} =~ /^[a-f0-9]{32}$/ and
	$params->{entityName} =~ /^.+$/ and
	$params->{sinceRun} =~ /^(|\d+)$/;

    my $session = _check_session($params->{sid})
	or die "Invalid session.\n";

    my $ret = _dbh()->selectall_arrayref("SELECT record_id,record FROM records WHERE username=? AND record_type=? AND updated_run>?", { Slice=>{} }, $session->{username}, $params->{entityName}, $params->{sinceRun} || 0)
	or die "Error getting record list.\n";

    return {
	records => {
	    map +($_->{record_id} => thaw($_->{record})),
		grep length($_->{record}), @$ret
	},
	deleted => [
	    map $_->{record_id},
		grep !length($_->{record}), @$ret
	],
    };
}

sub delete_all_records
{
    my $params = shift();

    die "Invalid request.\n" unless
	$params->{sid} =~ /^[a-f0-9]{32}$/ and
	$params->{entityName} =~ /^.+$/;

    my $session = _check_session($params->{sid})
	or die "Invalid session.\n";

    _dbh()->do("UPDATE records SET record=NULL,updated_run=? WHERE username=? AND record_type=?", undef, $session->{run}, $session->{username}, $params->{entityName})
	or die "Error deleting records.\n";

    return;
}

sub add_record
{
    my $params = shift();

    die "Invalid request.\n" unless
	$params->{sid} =~ /^[a-f0-9]{32}$/ and
	$params->{entityName} =~ /^.+$/ and
	$params->{recordIdentifier} =~ /^.+$/ and
	exists $params->{record};

    my $session = _check_session($params->{sid})
	or die "Invalid session.\n";

    _dbh()->do("REPLACE INTO records (username,updated_run,record_type,record_id,record) VALUES (?,?,?,?,?)", undef, $session->{username}, $session->{run}, $params->{entityName}, $params->{recordIdentifier}, freeze($params->{record})) + 0
	or die "Couldn't store record.\n";

    return;
}

sub delete_record
{
    my $params = shift();

    die "Invalid request.\n" unless
	$params->{sid} =~ /^[a-f0-9]{32}$/ and
	$params->{entityName} =~ /^.+$/ and
	$params->{recordIdentifier} =~ /^.+$/;

    my $session = _check_session($params->{sid})
	or die "Invalid session.\n";

    _dbh()->do("UPDATE records SET record=NULL,updated_run=? WHERE username=? AND record_type=? AND record_id=?", undef, $session->{run}, $session->{username}, $params->{entityName}, $params->{recordIdentifier})
	or die "Couldn't delete record.\n";

    return;
}

our $map = {
    validateAccount   => \&validate_account,
    queryStorageInfo  => \&query_storage_info,
    updateStorageInfo => \&update_storage_info,
    openSession       => \&open_session,
    closeSession      => \&close_session,
    getRecords        => \&get_records,
    deleteAllRecords  => \&delete_all_records,
    addRecord         => \&add_record,
    replaceRecord     => \&add_record,
    deleteRecord      => \&delete_record,
};

__END__

CREATE TABLE IF NOT EXISTS accounts (
 username varchar(255) not null,
 password varchar(255) not null,
 created int unsigned not null,
 public_access enum('read-only', 'read-write') default 'read-only' not null,
 public_password varchar(255) not null,
 run int unsigned not null,
 cid varchar(255),
 sid varchar(255),
 sid_access int unsigned,
 primary key (username),
 unique key (sid)
); 

CREATE TABLE IF NOT EXISTS records (
 username varchar(255) not null,
 updated_run int unsigned not null,
 record_type varchar(255) not null,
 record_id varchar(255) not null,
 record mediumblob,
 primary key (username),
 unique key (record_type, record_id),
 key (updated_run)
);
